highcharts-ng.js ➔ highchart   F
last analyzed

Complexity

Conditions 90

Size

Total Lines 400
Code Lines 242

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 242
dl 0
loc 400
rs 0
c 0
b 0
f 0
cc 90

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like highcharts-ng.js ➔ highchart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
if (typeof module !== 'undefined' && typeof exports !== 'undefined' && module.exports === exports){
2
  module.exports = 'highcharts-ng';
3
}
4
5
(function () {
6
  'use strict';
7
  /*global angular: false, Highcharts: false */
8
9
10
  angular.module('highcharts-ng', [])
11
    .provider('highchartsNG', highchartsNGProvider)
12
    .directive('highchart', ['highchartsNG', '$timeout', highchart]);
13
  
14
  function highchartsNGProvider(){
15
    var modules = [];
16
    var basePath = false;
17
    var lazyLoad = false;
18
    return {
19
      HIGHCHART: 'highcharts.js',
20
      HIGHSTOCK: 'stock/highstock.js',
21
      basePath: function (p) {
22
        basePath = p;
23
      },
24
      lazyLoad: function (list) {
25
        if (list === undefined) {
26
          modules = [this.HIGHCHART];
27
        } else {
28
          modules = list;
29
        }
30
        lazyLoad = true;
31
      },
32
      $get: ['$window', '$rootScope', function ($window, $rootScope) {
33
        if (!basePath) {
34
          basePath = (window.location.protocol === 'https:' ? 'https' : 'http') + '://code.highcharts.com/';
35
        }
36
        return highchartsNG($window, $rootScope, lazyLoad, basePath, modules);
37
      }]
38
    };
39
  }
40
  function highchartsNG($window, $rootScope, lazyload, basePath, modules) {
41
    var readyQueue = [];
42
    var loading = false;
43
    return {
44
      lazyLoad:lazyload,
45
      ready: function (callback, thisArg) {
46
        if (typeof $window.Highcharts !== 'undefined' || !lazyload) {
47
          callback();
48
        } else {
49
          readyQueue.push([callback, thisArg]);
50
          if (loading) {
51
            return;
52
          }
53
          loading = true;
54
          var self = this;
55
          if (typeof jQuery === 'undefined') {
56
            modules.unshift('adapters/standalone-framework.js');
57
          }
58
          var doWork = function () {
59
            if (modules.length === 0) {
60
              loading = false;
61
              $rootScope.$apply(function () {
62
                angular.forEach(readyQueue, function (e) {
63
                  // invoke callback passing 'thisArg'
64
                  e[0].apply(e[1], []);
65
                });
66
              });
67
            } else {
68
              var s = modules.shift();
69
              self.loadScript(s, doWork);
70
            }
71
          };
72
          doWork();
73
        }
74
      },
75
      loadScript: function (path, callback) {
76
        var s = document.createElement('script');
77
        s.type = 'text/javascript';
78
        s.src = basePath + path;
79
        s.onload = callback;
80
        document.getElementsByTagName('body')[0].appendChild(s);
81
      },
82
      //IE8 support
83
      indexOf: function (arr, find, i /*opt*/) {
84
        if (i === undefined) i = 0;
85
        if (i < 0) i += arr.length;
86
        if (i < 0) i = 0;
87
        for (var n = arr.length; i < n; i++)
88
          if (i in arr && arr[i] === find)
89
            return i;
90
        return -1;
91
      },
92
93
      prependMethod: function (obj, method, func) {
94
        var original = obj[method];
95
        obj[method] = function () {
96
          var args = Array.prototype.slice.call(arguments);
97
          func.apply(this, args);
98
          if (original) {
99
            return original.apply(this, args);
100
          } else {
101
            return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
102
          }
103
104
        };
105
      },
106
107
      deepExtend: function deepExtend(destination, source) {
108
        //Slightly strange behaviour in edge cases (e.g. passing in non objects)
109
        //But does the job for current use cases.
110
        if (angular.isArray(source)) {
111
          destination = angular.isArray(destination) ? destination : [];
112
          for (var i = 0; i < source.length; i++) {
113
            destination[i] = deepExtend(destination[i] || {}, source[i]);
114
          }
115
        } else if (angular.isObject(source)) {
116
          destination = angular.isObject(destination) ? destination : {};
117
          for (var property in source) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
118
            destination[property] = deepExtend(destination[property] || {}, source[property]);
119
          }
120
        } else {
121
          destination = source;
122
        }
123
        return destination;
124
      }
125
    };
126
  }
127
128
  function highchart(highchartsNGUtils, $timeout) {
129
130
    // acceptable shared state
131
    var seriesId = 0;
132
    var ensureIds = function (series) {
133
      var changed = false;
134
      angular.forEach(series, function(s) {
135
        if (!angular.isDefined(s.id)) {
136
          s.id = 'series-' + seriesId++;
137
          changed = true;
138
        }
139
      });
140
      return changed;
141
    };
142
143
    // immutable
144
    var axisNames = [ 'xAxis', 'yAxis' ];
145
    var chartTypeMap = {
146
      'stock': 'StockChart',
147
      'map':   'Map',
148
      'chart': 'Chart'
149
    };
150
151
    var getMergedOptions = function (scope, element, config) {
152
      var mergedOptions = {};
0 ignored issues
show
Unused Code introduced by
The assignment to variable mergedOptions seems to be never used. Consider removing it.
Loading history...
153
154
      var defaultOptions = {
155
        chart: {
156
          events: {}
157
        },
158
        title: {},
159
        subtitle: {},
160
        series: [],
161
        credits: {},
162
        plotOptions: {},
163
        navigator: {enabled: false},
164
        xAxis: {
165
          events: {}
166
        },
167
        yAxis: {
168
          events: {}
169
        }
170
      };
171
172
      if (config.options) {
173
        mergedOptions = highchartsNGUtils.deepExtend(defaultOptions, config.options);
174
      } else {
175
        mergedOptions = defaultOptions;
176
      }
177
      mergedOptions.chart.renderTo = element[0];
178
179
      angular.forEach(axisNames, function(axisName) {
180
        if(angular.isDefined(config[axisName])) {
181
          mergedOptions[axisName] = highchartsNGUtils.deepExtend(mergedOptions[axisName] || {}, config[axisName]);
182
183
          if(angular.isDefined(config[axisName].currentMin) ||
184
              angular.isDefined(config[axisName].currentMax)) {
185
186
            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'selection', function(e){
187
              var thisChart = this;
188
              if (e[axisName]) {
189
                scope.$apply(function () {
190
                  scope.config[axisName].currentMin = e[axisName][0].min;
191
                  scope.config[axisName].currentMax = e[axisName][0].max;
192
                });
193
              } else {
194
                //handle reset button - zoom out to all
195
                scope.$apply(function () {
196
                  scope.config[axisName].currentMin = thisChart[axisName][0].dataMin;
197
                  scope.config[axisName].currentMax = thisChart[axisName][0].dataMax;
198
                });
199
              }
200
            });
201
202
            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'addSeries', function(e){
203
              scope.config[axisName].currentMin = this[axisName][0].min || scope.config[axisName].currentMin;
204
              scope.config[axisName].currentMax = this[axisName][0].max || scope.config[axisName].currentMax;
205
            });
206
            highchartsNGUtils.prependMethod(mergedOptions[axisName].events, 'setExtremes', function (e) {
207
              if (e.trigger && e.trigger !== 'zoom') { // zoom trigger is handled by selection event
208
                $timeout(function () {
209
                  scope.config[axisName].currentMin = e.min;
210
                  scope.config[axisName].currentMax = e.max;
211
                  scope.config[axisName].min = e.min; // set min and max to adjust scrollbar/navigator
212
                  scope.config[axisName].max = e.max;
213
                }, 0);
214
              }
215
            });
216
          }
217
        }
218
      });
219
220
      if(config.title) {
221
        mergedOptions.title = config.title;
222
      }
223
      if (config.subtitle) {
224
        mergedOptions.subtitle = config.subtitle;
225
      }
226
      if (config.credits) {
227
        mergedOptions.credits = config.credits;
228
      }
229
      if(config.size) {
230
        if (config.size.width) {
231
          mergedOptions.chart.width = config.size.width;
232
        }
233
        if (config.size.height) {
234
          mergedOptions.chart.height = config.size.height;
235
        }
236
      }
237
      return mergedOptions;
238
    };
239
240
    var updateZoom = function (axis, modelAxis) {
241
      var extremes = axis.getExtremes();
242
      if(modelAxis.currentMin !== extremes.dataMin || modelAxis.currentMax !== extremes.dataMax) {
243
        if (axis.setExtremes) {
244
          axis.setExtremes(modelAxis.currentMin, modelAxis.currentMax, false);
245
        } else {
246
          axis.detachedsetExtremes(modelAxis.currentMin, modelAxis.currentMax, false);
247
        }
248
      }
249
    };
250
251
    var processExtremes = function(chart, axis, axisName) {
252
      if(axis.currentMin || axis.currentMax) {
253
        chart[axisName][0].setExtremes(axis.currentMin, axis.currentMax, true);
254
      }
255
    };
256
257
    var chartOptionsWithoutEasyOptions = function (options) {
258
      return angular.extend(
259
        highchartsNGUtils.deepExtend({}, options),
260
        { data: null, visible: null }
261
      );
262
    };
263
264
    var getChartType = function(scope) {
265
      if (scope.config === undefined) return 'Chart';
266
      return chartTypeMap[('' + scope.config.chartType).toLowerCase()] ||
267
             (scope.config.useHighStocks ? 'StockChart' : 'Chart');
268
    };
269
270
    var res = {
271
      restrict: 'EAC',
272
      replace: true,
273
      template: '<div></div>',
274
      scope: {
275
        config: '=',
276
        disableDataWatch: '='
277
      },
278
      link: function (scope, element, attrs) {
279
        // We keep some chart-specific variables here as a closure
280
        // instead of storing them on 'scope'.
281
282
        // prevSeriesOptions is maintained by processSeries
283
        var prevSeriesOptions = {};
284
285
        var processSeries = function(series, seriesOld) {
286
          var i;
287
          var ids = [];
288
289
          if(series) {
290
            var setIds = ensureIds(series);
291
            if(setIds && !scope.disableDataWatch) {
292
              //If we have set some ids this will trigger another digest cycle.
293
              //In this scenario just return early and let the next cycle take care of changes
294
              return false;
295
            }
296
297
            //Find series to add or update
298
            angular.forEach(series, function(s, idx) {
299
              ids.push(s.id);
300
              var chartSeries = chart.get(s.id);
301
              if (chartSeries) {
302
                if (!angular.equals(prevSeriesOptions[s.id], chartOptionsWithoutEasyOptions(s))) {
303
                  chartSeries.update(angular.copy(s), false);
304
                } else {
305
                  if (s.visible !== undefined && chartSeries.visible !== s.visible) {
306
                    chartSeries.setVisible(s.visible, false);
307
                  }
308
                  
309
                  // Make sure the current series index can be accessed in seriesOld
310
                  if (idx < seriesOld.length) {
311
                    var sOld = seriesOld[idx];
312
                    var sCopy = angular.copy(sOld);
313
                    
314
                    // Get the latest data point from the new series
315
                    var ptNew = s.data[s.data.length - 1];
316
                    
317
                    // Check if the new and old series are identical with the latest data point added
318
                    // If so, call addPoint without shifting
319
                    sCopy.data.push(ptNew);
320
                    if (angular.equals(sCopy, s)) {
321
                      chartSeries.addPoint(ptNew, false);
322
                    }
323
                    
324
                    // Check if the data change was a push and shift operation
325
                    // If so, call addPoint WITH shifting
326
                    else {
327
                      sCopy.data.shift();
328
                      if (angular.equals(sCopy, s)) {
329
                        chartSeries.addPoint(ptNew, false, true);
330
                      }
331
                      else {
332
                        chartSeries.setData(angular.copy(s.data), false);
333
                      }
334
                    }
335
                  }
336
                  else {
337
                    chartSeries.setData(angular.copy(s.data), false);
338
                  }
339
                }
340
              } else {
341
                chart.addSeries(angular.copy(s), false);
342
              }
343
              prevSeriesOptions[s.id] = chartOptionsWithoutEasyOptions(s);
344
            });
345
346
            //  Shows no data text if all series are empty
347
            if(scope.config.noData) {
348
              var chartContainsData = false;
349
350
              for(i = 0; i < series.length; i++) {
351
                if (series[i].data && series[i].data.length > 0) {
352
                  chartContainsData = true;
353
354
                  break;
355
                }
356
              }
357
358
              if (!chartContainsData) {
359
                chart.showLoading(scope.config.noData);
360
              } else {
361
                chart.hideLoading();
362
              }
363
            }
364
          }
365
366
          //Now remove any missing series
367
          for(i = chart.series.length - 1; i >= 0; i--) {
368
            var s = chart.series[i];
369
            if (s.options.id !== 'highcharts-navigator-series' && highchartsNGUtils.indexOf(ids, s.options.id) < 0) {
370
              s.remove(false);
371
            }
372
          }
373
374
          return true;
375
        };
376
377
        // chart is maintained by initChart
378
        var chart = false;
379
        var initChart = function() {
380
          if (chart) chart.destroy();
381
          prevSeriesOptions = {};
382
          var config = scope.config || {};
383
          var mergedOptions = getMergedOptions(scope, element, config);
384
          var func = config.func || undefined;
385
          var chartType = getChartType(scope);
386
387
          chart = new Highcharts[chartType](mergedOptions, func);
388
389
          for (var i = 0; i < axisNames.length; i++) {
390
            if (config[axisNames[i]]) {
391
              processExtremes(chart, config[axisNames[i]], axisNames[i]);
392
            }
393
          }
394
          if(config.loading) {
395
            chart.showLoading();
396
          }
397
          config.getHighcharts = function() {
398
            return chart;
399
          };
400
401
        };
402
        initChart();
403
404
405
        if(scope.disableDataWatch){
406
          scope.$watchCollection('config.series', function (newSeries, oldSeries) {
407
            processSeries(newSeries);
408
            chart.redraw();
409
          });
410
        } else {
411
          scope.$watch('config.series', function (newSeries, oldSeries) {
412
            var needsRedraw = processSeries(newSeries, oldSeries);
413
            if(needsRedraw) {
414
              chart.redraw();
415
            }
416
          }, true);
417
        }
418
419
        scope.$watch('config.title', function (newTitle) {
420
          chart.setTitle(newTitle, true);
421
        }, true);
422
423
        scope.$watch('config.subtitle', function (newSubtitle) {
424
          chart.setTitle(true, newSubtitle);
425
        }, true);
426
427
        scope.$watch('config.loading', function (loading) {
428
          if(loading) {
429
            chart.showLoading(loading === true ? null : loading);
430
          } else {
431
            chart.hideLoading();
432
          }
433
        });
434
        scope.$watch('config.noData', function (noData) {
435
          if(scope.config && scope.config.loading) {
436
            chart.showLoading(noData);
437
          }
438
        }, true);
439
440
        scope.$watch('config.credits.enabled', function (enabled) {
441
          if (enabled) {
442
            chart.credits.show();
443
          } else if (chart.credits) {
444
            chart.credits.hide();
445
          }
446
        });
447
448
        scope.$watch(getChartType, function (chartType, oldChartType) {
449
          if (chartType === oldChartType) return;
450
          initChart();
451
        });
452
453
        angular.forEach(axisNames, function(axisName) {
454
          scope.$watch('config.' + axisName, function(newAxes) {
455
            if (!newAxes) {
456
              return;
457
            }
458
459
            if (angular.isArray(newAxes)) {
460
461
              for (var axisIndex = 0; axisIndex < newAxes.length; axisIndex++) {
462
                var axis = newAxes[axisIndex];
463
464
                if (axisIndex < chart[axisName].length) {
465
                  chart[axisName][axisIndex].update(axis, false);
466
                  updateZoom(chart[axisName][axisIndex], angular.copy(axis));
467
                }
468
469
              }
470
471
            } else {
472
              // update single axis
473
              chart[axisName][0].update(newAxes, false);
474
              updateZoom(chart[axisName][0], angular.copy(newAxes));
475
            }
476
477
            chart.redraw();
478
          }, true);
479
        });
480
        scope.$watch('config.options', function (newOptions, oldOptions, scope) {
481
          //do nothing when called on registration
482
          if (newOptions === oldOptions) return;
483
          initChart();
484
          processSeries(scope.config.series);
485
          chart.redraw();
486
        }, true);
487
488
        scope.$watch('config.size', function (newSize, oldSize) {
489
          if(newSize === oldSize) return;
490
          if(newSize) {
491
            chart.setSize(newSize.width || chart.chartWidth, newSize.height || chart.chartHeight);
492
          }
493
        }, true);
494
495
        scope.$on('highchartsng.reflow', function () {
496
          chart.reflow();
497
        });
498
499
        scope.$on('$destroy', function() {
500
          if (chart) {
501
            try{
502
              chart.destroy();
503
            }catch(ex){
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
504
              // fail silently as highcharts will throw exception if element doesn't exist
505
            }
506
507
            $timeout(function(){
508
              element.remove();
509
            }, 0);
510
          }
511
        });
512
513
      }
514
    };
515
    
516
    // override link fn if lazy loading is enabled
517
    if(highchartsNGUtils.lazyLoad){
518
      var oldLink = res.link;
519
      res.link = function(){
520
        var args = arguments;
521
        highchartsNGUtils.ready(function(){
522
          oldLink.apply(this, args);
523
        }, this);
524
      };
525
    }
526
    return res;
527
  }
528
}());
529